package com.yeskery.nut.scan.controller;

import com.yeskery.nut.annotation.web.*;
import com.yeskery.nut.application.NutApplication;
import com.yeskery.nut.core.Controller;
import com.yeskery.nut.core.*;
import com.yeskery.nut.extend.auth.CrossConfiguration;
import com.yeskery.nut.scan.AnnotationController;
import com.yeskery.nut.scan.AnnotationHandler;
import com.yeskery.nut.scan.BeanAnnotationScanMetadata;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.*;

/**
 * 基于注解的Controller处理器
 * @author sprout
 * 2022-06-10 14:12
 */
public class AnnotationControllerHandler implements AnnotationHandler {

    /** Nut应用对象 */
    private final NutApplication nutApplication;

    /** Controller管理器 */
    private final ControllerManager controllerManager;

    /**
     * 构建基于注解的Controller扫描器
     * @param nutApplication Nut应用对象
     * @param controllerManager Controller管理器
     */
    public AnnotationControllerHandler(NutApplication nutApplication, ControllerManager controllerManager) {
        this.nutApplication = nutApplication;
        this.controllerManager = controllerManager;
    }

    @Override
    public void handle(Collection<Class<?>> beanClassCollection, Collection<BeanAnnotationScanMetadata> beanMetadataCollection) {
        Collection<ControllerCombinationMetadata> metadataCollection = getControllerCombinationMetadataCollection(beanClassCollection);
        for (ControllerCombinationMetadata metadata : metadataCollection) {
            AnnotationController controller = new AnnotationController();
            com.yeskery.nut.core.Controller proxyController = (Controller) Proxy.newProxyInstance(controller.getClass().getClassLoader(),
                    controller.getClass().getInterfaces(),
                    new AnnotationControllerInvocationHandler(nutApplication, controller, metadata.getHoldMethodAttributesMap()));

            ControllerMetadata controllerMetadata = metadata.getControllerMetadata();
            controllerManager.registerController(proxyController, controllerMetadata.getControllerSource(),
                    controllerMetadata.getPath(), controllerMetadata.getAlias());
        }
    }

    /**
     * 获取所有的Controller 组合元信息集合
     * @param collection 注解类集合
     * @return Controller 组合元信息
     */
    private Collection<ControllerCombinationMetadata> getControllerCombinationMetadataCollection(Collection<Class<?>> collection) {
        Map<String, ControllerCombinationMetadata> combinationMetadataPathMap = new HashMap<>(16);

        // 处理Controller注解的目标类
        for (Class<?> clazz : collection) {
            // 处理基础的请求路径
            String[] baseUris = {""};
            RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
            if (requestMapping != null) {
                baseUris = getRequestPaths(requestMapping.value(), requestMapping.path());
            }

            // 处理具体的请求方法
            for (Method method : clazz.getMethods()) {
                if (!method.isAnnotationPresent(RequestMapping.class) && !method.isAnnotationPresent(GetMapping.class)
                        && !method.isAnnotationPresent(PostMapping.class) && !method.isAnnotationPresent(PutMapping.class)
                        && !method.isAnnotationPresent(DeleteMapping.class)) {
                    continue;
                }
                String[] requestPaths;
                com.yeskery.nut.core.Method[] methods;
                String[] params = getArrayParameterFormRequestAnnotation(Optional.ofNullable(requestMapping).map(RequestMapping::params));
                String[] headers = getArrayParameterFormRequestAnnotation(Optional.ofNullable(requestMapping).map(RequestMapping::headers));
                String[] consumes = getArrayParameterFormRequestAnnotation(Optional.ofNullable(requestMapping).map(RequestMapping::consumes));
                String[] produces = getArrayParameterFormRequestAnnotation(Optional.ofNullable(requestMapping).map(RequestMapping::produces));
                if (method.isAnnotationPresent(RequestMapping.class)) {
                    RequestMapping methodRequestMapping = method.getAnnotation(RequestMapping.class);
                    requestPaths = getRequestPaths(methodRequestMapping.value(), methodRequestMapping.path());
                    methods = methodRequestMapping.method();
                    params = getArrayParameterFormRequestAnnotation(params, Optional.of(methodRequestMapping).map(RequestMapping::params));
                    headers = getArrayParameterFormRequestAnnotation(headers, Optional.of(methodRequestMapping).map(RequestMapping::headers));
                    consumes = getArrayParameterFormRequestAnnotation(consumes, Optional.of(methodRequestMapping).map(RequestMapping::consumes));
                    produces = getArrayParameterFormRequestAnnotation(produces, Optional.of(methodRequestMapping).map(RequestMapping::produces));
                } else if (method.isAnnotationPresent(GetMapping.class)) {
                    GetMapping getMapping = method.getAnnotation(GetMapping.class);
                    requestPaths = getRequestPaths(getMapping.value(), getMapping.path());
                    methods = new com.yeskery.nut.core.Method[] {com.yeskery.nut.core.Method.GET};
                    params = getArrayParameterFormRequestAnnotation(params, Optional.of(getMapping).map(GetMapping::params));
                    headers = getArrayParameterFormRequestAnnotation(headers, Optional.of(getMapping).map(GetMapping::headers));
                    consumes = getArrayParameterFormRequestAnnotation(consumes, Optional.of(getMapping).map(GetMapping::consumes));
                    produces = getArrayParameterFormRequestAnnotation(produces, Optional.of(getMapping).map(GetMapping::produces));
                } else if (method.isAnnotationPresent(PostMapping.class)) {
                    PostMapping postMapping = method.getAnnotation(PostMapping.class);
                    requestPaths = getRequestPaths(postMapping.value(), postMapping.path());
                    methods = new com.yeskery.nut.core.Method[] {com.yeskery.nut.core.Method.POST};
                    params = getArrayParameterFormRequestAnnotation(params, Optional.of(postMapping).map(PostMapping::params));
                    headers = getArrayParameterFormRequestAnnotation(headers, Optional.of(postMapping).map(PostMapping::headers));
                    consumes = getArrayParameterFormRequestAnnotation(consumes, Optional.of(postMapping).map(PostMapping::consumes));
                    produces = getArrayParameterFormRequestAnnotation(produces, Optional.of(postMapping).map(PostMapping::produces));
                } else if (method.isAnnotationPresent(PutMapping.class)) {
                    PutMapping putMapping = method.getAnnotation(PutMapping.class);
                    requestPaths = getRequestPaths(putMapping.value(), putMapping.path());
                    methods = new com.yeskery.nut.core.Method[] {com.yeskery.nut.core.Method.PUT};
                    params = getArrayParameterFormRequestAnnotation(params, Optional.of(putMapping).map(PutMapping::params));
                    headers = getArrayParameterFormRequestAnnotation(headers, Optional.of(putMapping).map(PutMapping::headers));
                    consumes = getArrayParameterFormRequestAnnotation(consumes, Optional.of(putMapping).map(PutMapping::consumes));
                    produces = getArrayParameterFormRequestAnnotation(produces, Optional.of(putMapping).map(PutMapping::produces));
                } else {
                    DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
                    requestPaths = getRequestPaths(deleteMapping.value(), deleteMapping.path());
                    methods = new com.yeskery.nut.core.Method[] {com.yeskery.nut.core.Method.DELETE};
                    params = getArrayParameterFormRequestAnnotation(params, Optional.of(deleteMapping).map(DeleteMapping::params));
                    headers = getArrayParameterFormRequestAnnotation(headers, Optional.of(deleteMapping).map(DeleteMapping::headers));
                    consumes = getArrayParameterFormRequestAnnotation(consumes, Optional.of(deleteMapping).map(DeleteMapping::consumes));
                    produces = getArrayParameterFormRequestAnnotation(produces, Optional.of(deleteMapping).map(DeleteMapping::produces));
                }
                String path = baseUris[0] + requestPaths[0];
                // 注解注册的Controller不允许和其他方法注册的Controller一起使用
                com.yeskery.nut.core.Controller controller = controllerManager.getController(path);
                if (controller != null) {
                    throw new NutException("Do Not Support Mixed Registration Methods Bean.");
                }

                // 相同路径共有一个组合元数据对象
                ControllerCombinationMetadata combinationMetadata = combinationMetadataPathMap.get(path);
                if (combinationMetadata != null) {
                    Map<com.yeskery.nut.core.Method, AnnotationRequestMethodAttributes> holdMethodAttributesMap
                            = combinationMetadata.getHoldMethodAttributesMap();
                    for (com.yeskery.nut.core.Method requestMethod : methods) {
                        if (holdMethodAttributesMap.containsKey(requestMethod)) {
                            throw new NutException("The Request Path[" + path + "] Method[" + requestMethod + "] Has Been Registered.");
                        }
                        AnnotationRequestMethodAttributes attributes = getAnnotationRequestMethodAttributes(clazz, method);
                        attributes.setParams(params);
                        attributes.setHeaders(headers);
                        attributes.setConsumes(consumes);
                        attributes.setProduces(produces);
                        holdMethodAttributesMap.put(requestMethod, attributes);
                    }
                } else {
                    String[] alias = getRequestAlias(baseUris, requestPaths);
                    ControllerCombinationMetadata controllerCombinationMetadata = new ControllerCombinationMetadata();
                    Map<com.yeskery.nut.core.Method, AnnotationRequestMethodAttributes> holdMethodMap = new HashMap<>(16);
                    controllerCombinationMetadata.setHoldMethodAttributesMap(holdMethodMap);
                    DefaultControllerMeta controllerMetadata = new DefaultControllerMeta();
                    controllerMetadata.setPath(path);
                    controllerMetadata.setAlias(alias);
                    controllerMetadata.setControllerSource(ControllerSource.ANNOTATION);
                    controllerCombinationMetadata.setControllerMetadata(controllerMetadata);
                    for (com.yeskery.nut.core.Method requestMethod : methods) {
                        if (holdMethodMap.containsKey(requestMethod)) {
                            throw new NutException("The Request Method Has Been Registered.");
                        }
                        AnnotationRequestMethodAttributes attributes = getAnnotationRequestMethodAttributes(clazz, method);
                        attributes.setParams(params);
                        attributes.setHeaders(headers);
                        attributes.setConsumes(consumes);
                        attributes.setProduces(produces);
                        holdMethodMap.put(requestMethod, attributes);
                    }

                    combinationMetadataPathMap.put(path, controllerCombinationMetadata);
                }
            }
        }

        return combinationMetadataPathMap.values();
    }

    /**
     * 获取注解请求方法属性
     * @param method 处理方法
     * @return 注解请求方法属性
     */
    private AnnotationRequestMethodAttributes getAnnotationRequestMethodAttributes(Class<?> type, Method method) {
        AnnotationRequestMethodAttributes attributes = new AnnotationRequestMethodAttributes();
        Map<Integer, List<Annotation>> annotationMap = new HashMap<>(16);
        attributes.setType(type);
        attributes.setMethod(method);
        if (type.isAnnotationPresent(RestController.class)) {
            attributes.setResponseBody(true);
        } else {
            attributes.setResponseBody(method.isAnnotationPresent(ResponseBody.class));
        }
        ResponseStatus responseStatus = method.getAnnotation(ResponseStatus.class);
        if (responseStatus != null) {
            int code = responseStatus.value();
            int statusCode = responseStatus.status().getCode();
            if (code != statusCode) {
                code = code == ResponseCode.OK.getCode() ? statusCode : code;
            }
            attributes.setResponseCode(code);
        }
        attributes.setCrossConfigurations(getCrossConfiguration(method));
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            List<Annotation> annotationList = new ArrayList<>();
            RequestBody requestBody = parameter.getAnnotation(RequestBody.class);
            if (requestBody != null) {
                annotationList.add(requestBody);
            }

            RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
            if (requestParam != null) {
                annotationList.add(requestParam);
            }

            CookieValue cookieValue = parameter.getAnnotation(CookieValue.class);
            if (cookieValue != null) {
                annotationList.add(cookieValue);
            }

            RequestHeader requestHeader = parameter.getAnnotation(RequestHeader.class);
            if (requestHeader != null) {
                annotationList.add(requestHeader);
            }

            RequestAttribute requestAttribute = parameter.getAnnotation(RequestAttribute.class);
            if (requestAttribute != null) {
                annotationList.add(requestAttribute);
            }

            SessionAttribute sessionAttribute = parameter.getAnnotation(SessionAttribute.class);
            if (sessionAttribute != null) {
                annotationList.add(sessionAttribute);
            }

            PathVariable pathVariable = parameter.getAnnotation(PathVariable.class);
            if (pathVariable != null) {
                annotationList.add(pathVariable);
            }

            if (!annotationList.isEmpty()) {
                annotationMap.put(i, annotationList);
            }
        }
        attributes.setAnnotationIndexMap(annotationMap);
        return attributes;
    }

    /**
     * 获取跨域注解配置信息
     * @param method 请求的方法
     * @return 跨域注解配置信息
     */
    private Collection<CrossConfiguration> getCrossConfiguration(java.lang.reflect.Method method) {
        CrossOrigin crossOrigin = method.getAnnotation(CrossOrigin.class);
        crossOrigin = crossOrigin == null ? method.getDeclaringClass().getAnnotation(CrossOrigin.class) : crossOrigin;
        if (crossOrigin == null || crossOrigin.allowedOrigins().length == 0) {
            return Collections.emptyList();
        }
        List<CrossConfiguration> crossConfigurations = new ArrayList<>(crossOrigin.allowedOrigins().length);
        for (String allowedOrigin : crossOrigin.allowedOrigins()) {
            CrossConfiguration crossConfiguration = new CrossConfiguration();
            crossConfiguration.setAllowedOrigins(allowedOrigin);
            crossConfiguration.setAllowedHeaders(String.join(",", crossOrigin.allowedHeaders()));
            crossConfiguration.setAllowedMethods(String.join(",", crossOrigin.allowedMethods()));
            crossConfiguration.setAllowedCredentials(crossOrigin.allowedCredentials());
            crossConfiguration.setMaxAge(crossOrigin.maxAge());
            crossConfigurations.add(crossConfiguration);
        }
        return crossConfigurations;
    }

    /**
     * 获取基础请求路径
     * @param paths 基础请求路径
     * @param alias 别名
     * @return 基础请求路径
     */
    private String[] getRequestPaths(String[] paths, String[] alias) {
        String[] baseUris = {""};
        if (paths.length > 0) {
            baseUris = paths;
        }
        if (alias.length > 0) {
            baseUris = alias;
        }
        for (int i = 0; i < baseUris.length; i++) {
            String uri = baseUris[i];
            if (!uri.isEmpty() && !uri.startsWith("/")) {
                baseUris[i] = "/" + uri;
            }
        }
        return baseUris;
    }

    /**
     * 获取请求别名
     * @param basePaths 基础路径
     * @param requestPaths 请求路径
     * @return 请求别名
     */
    private String[] getRequestAlias(String[] basePaths, String[] requestPaths) {
        if (requestPaths.length <= 1) {
            return new String[0];
        }
        List<String> alias = new LinkedList<>();
        for (String basePath : basePaths) {
            for (int j = 1; j < requestPaths.length; j++) {
                String uri;
                if (!basePath.endsWith("/") && !requestPaths[j].startsWith("/")) {
                    uri = basePath + "/" + requestPaths[j];
                } else {
                    uri = basePath + requestPaths[j];
                }
                if (!uri.startsWith("/")) {
                    uri = "/" + uri;
                }
                alias.add(new Path(uri).getPath());
            }
        }
        return alias.toArray(new String[0]);
    }

    /**
     * 获取数组参数
     * @param arrayOptional 数组参数Optional
     * @return 数组参数
     */
    private String[] getArrayParameterFormRequestAnnotation(Optional<String[]> arrayOptional) {
        if (arrayOptional == null || !arrayOptional.isPresent()) {
            return new String[0];
        }
        String[] array = arrayOptional.get();
        return array.length == 0 ? new String[0] : array;
    }

    /**
     * 获取数组参数
     * @param originalArray 原始数组参数
     * @param targetArrayOptional 目标数组参数Optional
     * @return 数组参数
     */
    private String[] getArrayParameterFormRequestAnnotation(String[] originalArray, Optional<String[]> targetArrayOptional) {
        if (targetArrayOptional == null || !targetArrayOptional.isPresent()) {
            return originalArray;
        }
        String[] targetArray = targetArrayOptional.get();
        return targetArray.length == 0 ? originalArray : targetArray;
    }
}
